"""
通用标定功能显示窗口（PySide2）

功能目标：
- 顶部/主区域：显示连接车辆执行命令的文本结果（支持实时更新与滚动）。
- 底部区域：根据传入的标定菜单类型动态生成功能按钮（下载日志/下载pcd/显示pcd）。

实现约束：
- 使用Python 3.10，遵循PEP 8。
- 使用 `/resource/qss/calibration_action_window.qss` 统一管理按钮与窗口样式。
- 使用语言管理器实现中英文切换（`mooetool/resource/languages/mooetool.json`）。
- 按钮具体业务逻辑可后续实现；当前仅发出信号并记录日志，不阻塞UI。

线程安全：
- 提供信号 `appendLogSignal` 支持跨线程安全追加文本到日志区域。

测试接口：
- 暴露 `available_actions`，便于单元测试校验不同菜单类型的按钮数量与类型。
"""

from __future__ import annotations

from typing import Dict, List, Callable, Optional, Any
from pathlib import Path

from PySide2.QtCore import Qt, Signal, Slot, QTimer, QThread
from PySide2.QtWidgets import (
    QWidget,
    QVBoxLayout,
    QPlainTextEdit,
    QFrame,
    QHBoxLayout,
    QPushButton,
    QMessageBox,
)

from mooetoolapp.handlercore.language_manager import LanguageManager
from mooetoolapp.handlercore.theme_manager import ThemeManager
from mooetoolapp.handlercore.signal_manager import create_signal_manager
from mooetoolapp.handlercore.calibration_command_executor import CalibrationCommandExecutor
from mooetoolapp.handlercore.pcd_viewer_vispy import PCDViewerWindow
from mooetoolapp.handlercore.calibration_download_manager import CalibrationDownloadManager
from gglobal import logger, config


class CalibrationCloseWorker(QThread):
    """关闭窗口专用线程

    目标：
    - 将较慢的停止流程（发送 Ctrl+C 等）放到后台线程执行；
    - 避免主UI线程卡顿与 closeEvent 重入。
    """

    # 关闭流程完成信号（供窗口触发最终关闭）
    close_done = Signal()

    def __init__(self, executor: CalibrationCommandExecutor, parent: Optional[QObject] = None) -> None:
        super().__init__(parent)
        self._executor = executor

    def run(self) -> None:
        try:
            if self._executor:
                # 优雅停止：发送 Ctrl+C 并等待一段时间
                try:
                    self._executor.stop(force=False)
                except Exception:
                    pass
                # 轮询等待工作线程自行结束（最多约2秒）
                for _ in range(20):
                    try:
                        if not self._executor.is_running():
                            break
                    except Exception:
                        break
                    QThread.msleep(100)
                # 若仍在运行，执行强制终止（避免UI阻塞）
                try:
                    if self._executor.is_running():
                        self._executor.stop(force=True)
                    
                except Exception:
                    pass
        except Exception:
            pass
        finally:
            try:
                self.close_done.emit()
            except Exception:
                pass


class CalibrationActionWindow(QWidget):
    """通用标定功能显示窗口

    参数：
    - calibration_type: 标定类型（用于决定底部按钮集合）
    - calibration_key: 当前标定项键（用于日志与业务回调标识）
    - parent: 父窗口

    信号：
    - actionTriggered(str): 发出动作类型（download_log/download_pcd/show_pcd）
    - appendLogSignal(str): 供跨线程安全地向日志区域追加文本
    """

    # 动作类型常量（中文注释，英文标识便于业务层判断）
    ACTION_DOWNLOAD_LOG = "download_log"
    ACTION_DOWNLOAD_PCD = "download_pcd"
    ACTION_SHOW_PCD = "show_pcd"

    # UI 文本键（统一通过语言管理器获取）
    UI_KEY_WINDOW_TITLE = "calib_action_window_title"
    UI_KEY_LOG_PLACEHOLDER = "calib_action_log_placeholder"
    UI_KEY_DOWNLOAD_LOG = "calib_action_download_log"
    UI_KEY_DOWNLOAD_PCD = "calib_action_download_pcd"
    UI_KEY_SHOW_PCD = "calib_action_show_pcd"

    # 根据标定类型，决定显示的按钮集合（类型与业务键分离）
    TYPE_BUTTON_MAP: Dict[str, List[str]] = {
        # 仅显示“下载日志”
        "calibration_menu_laser_yaw": [ACTION_DOWNLOAD_LOG],
        "calibration_menu_fork_camera_sub1": [ACTION_DOWNLOAD_LOG],
        "calibration_menu_recognition_camera_sub1": [ACTION_DOWNLOAD_LOG,ACTION_DOWNLOAD_PCD, ACTION_SHOW_PCD],
        # 显示“三个按钮”
        "calibration_menu_laser_joint": [ACTION_DOWNLOAD_LOG, ACTION_DOWNLOAD_PCD, ACTION_SHOW_PCD],
        "calibration_menu_mid360": [ACTION_DOWNLOAD_LOG, ACTION_DOWNLOAD_PCD, ACTION_SHOW_PCD],
        "calibration_menu_recognition_camera_sub2": [ACTION_DOWNLOAD_LOG, ACTION_DOWNLOAD_PCD, ACTION_SHOW_PCD],
    }

    # PCD 文件映射：根据类型与键决定需要下载的文件名列表
    PCD_FILES_MAP: Dict[str, Dict[str, List[str]]] = {
        "calibration_menu_laser_joint": {
            "single": [
                "middle_down_aligned_cloud.pcd",
                "middle_lidar_up_cloud.pcd",
            ],
            "left": [
                "left_down_aligned_cloud.pcd",
                "left_lidar_up_cloud.pcd",
            ],
            "right": [
                "right_down_aligned_cloud.pcd",
                "right_lidar_up_cloud.pcd",
            ],
        },
        "calibration_menu_mid360": {
            "single": [
                "livox_aligned_cloud.pcd",
                "lidar_up_cloud.pcd",
            ],
        },
        "calibration_menu_recognition_camera_sub1": {
            "single": [
                "livox_roi_cloud.pcd",
                "livox_result_cloud.pcd",
            ],
        },
        "calibration_menu_recognition_camera_sub2": {
            "left": [
                "livox_left_cloud.pcd",
                "RS_left_aligned_cloud.pcd",
            ],
            "right": [
                "livox_right_cloud.pcd",
                "RS_right_aligned_cloud.pcd",
            ],
        },
    }

    # 跨线程安全日志信号（其它线程调用 emit 即可安全更新 UI）
    appendLogSignal = Signal(str)
    # 动作触发信号（业务层可连接此信号以执行具体功能）
    actionTriggered = Signal(str)

    def __init__(
        self,
        calibration_type: str,
        calibration_key: str,
        on_action: Optional[Callable[[str, str], None]] = None,
        vehicle_connection_manager: Optional[Any] = None,
        parent: QWidget | None = None,
    ) -> None:
        super().__init__(parent)
        # 设置对象名，方便在全局QSS中进行选择器管理
        self.setObjectName("calibration_action_window")

        # 标定类型与业务键
        self.calibration_type = calibration_type
        # 标定菜单键（如：某具体菜单项的唯一键）
        self.calibration_key = calibration_key

        # 管理器（语言与主题）
        self.language_manager = LanguageManager()
        self.theme_manager = ThemeManager()

        # 外部回调：点击按钮后，传递(当前标定功能键, 动作类型)
        self.on_action = on_action
        # 车辆连接管理器（用于在窗口打开后启动标定命令）
        self.vehicle_connection_manager = vehicle_connection_manager
        # 当前标定执行器实例（用于生命周期管理）
        self._calibration_executor: Optional[CalibrationCommandExecutor] = None

        # 记录当前窗口可用的动作集合，便于测试与业务层查询（基于类型决定）
        self.available_actions: List[str] = self.TYPE_BUTTON_MAP.get(self.calibration_type, [self.ACTION_DOWNLOAD_LOG])

        # 组件字典（便于后续更新文本与样式）
        self.buttons: Dict[str, QPushButton] = {}

        self._signal_manager = create_signal_manager("CalibrationActionWindow", self)
        self._init_ui()
        self._translate_ui()

        # 连接跨线程日志信号到主线程插入逻辑
        self._signal_manager.register_connection("append_log", self.appendLogSignal, self._append_log, Qt.QueuedConnection)
        self._signal_manager.connect_signal("append_log")

        # 窗口基础属性（应用模态对话框）
        # 目标：显示系统标题栏与控制按钮，并在打开后禁用其它窗口点击，仅允许操作本窗口。
        self.setMinimumSize(640, 420)
        # 设置对话框样式与系统装饰（标题栏/系统菜单/最小化/最大化）
        self.setWindowFlags(
            self.windowFlags()
            | Qt.Dialog
            | Qt.WindowTitleHint
            | Qt.WindowSystemMenuHint
            | Qt.WindowMinimizeButtonHint
            | Qt.WindowMaximizeButtonHint
        )
        # 设置为应用模态：阻止与应用中其它窗口交互，但不阻塞事件循环
        self.setWindowModality(Qt.ApplicationModal)
        # 关闭时自动删除，避免重复打开造成内存泄漏
        self.setAttribute(Qt.WA_DeleteOnClose, True)

        logger.info(
            "CalibrationActionWindow initialized: type=%s key=%s actions=%s",
            self.calibration_type,
            self.calibration_key,
            self.available_actions,
        )

        # 登记连接持有者，避免窗口关闭时误断开连接（引用计数机制）
        try:
            if self.vehicle_connection_manager and hasattr(self.vehicle_connection_manager, 'acquire'):
                self.vehicle_connection_manager.acquire(self)
                logger.info("CalibrationActionWindow: 已登记为连接持有者")
        except Exception as e:
            logger.warning(f"CalibrationActionWindow: 登记连接持有者失败: {e}")

        # 关闭流程控制：使用实例定时器避免对象销毁后的回调触发
        try:
            self._closing_in_progress = False
            self._final_close_ready = False  # 最终关闭标志：二次 closeEvent 直接接受
            self._close_worker: Optional[CalibrationCloseWorker] = None
            self._close_fallback_timer = QTimer(self)
            self._close_fallback_timer.setSingleShot(True)
            self._close_fallback_timer.timeout.connect(self._force_close_if_still_running)
            self._cleanup_retry_timer = QTimer(self)
            self._cleanup_retry_timer.setSingleShot(True)
            self._cleanup_retry_timer.timeout.connect(self._cleanup_executor)
        except Exception:
            pass

        # 在窗口创建后调度启动标定命令（确保UI已呈现）
        try:
            QTimer.singleShot(0, self._start_calibration_if_possible)
        except Exception:
            pass

        # PCD 查看器实例由 _open_pcd_viewer 统一管理（按需创建，关闭即销毁）
        # 为避免多处复用造成生命周期混乱，这里不持有额外引用。

    def _init_ui(self) -> None:
        """初始化界面布局与控件。"""
        layout = QVBoxLayout(self)
        layout.setContentsMargins(8, 8, 8, 8)
        layout.setSpacing(8)

        # 日志显示框（只读，性能更好的纯文本控件）
        self.log_view = QPlainTextEdit(self)
        self.log_view.setObjectName("logView")
        self.log_view.setReadOnly(True)
        self.log_view.setLineWrapMode(QPlainTextEdit.NoWrap)
        layout.addWidget(self.log_view, stretch=1)

        # 底部按钮区域
        self.action_bar = QFrame(self)
        self.action_bar.setObjectName("actionBar")
        action_layout = QHBoxLayout(self.action_bar)
        action_layout.setContentsMargins(0, 0, 0, 0)
        action_layout.setSpacing(8)
        action_layout.addStretch(1)

        # 根据动作集合创建按钮
        for action in self.available_actions:
            btn = self._create_button_for_action(action)
            action_layout.addWidget(btn)
            self.buttons[action] = btn

        layout.addWidget(self.action_bar, stretch=0)

    def _start_calibration_if_possible(self) -> None:
        """窗口打开后尝试启动标定命令（使用连接管理器获取客户端线程）。"""
        try:
            if not self.vehicle_connection_manager:
                logger.warning("未传入车辆连接管理器，无法执行标定命令")
                return
            client_thread = None
            try:
                client_thread = self.vehicle_connection_manager.get_client_thread()
            except Exception as exc:
                logger.exception(f"获取车辆客户端线程失败: {exc}")
                client_thread = None

            if client_thread is None:
                logger.warning("未找到车辆客户端线程，无法执行标定命令")
                return

            # 保存执行器实例并启动执行（输出通过信号实时显示到日志）
            # 不将执行器绑定为窗口子对象，避免窗口销毁时强制销毁仍在运行的线程
            self._calibration_executor = CalibrationCommandExecutor()
            self._calibration_executor.start_on_window_open(
                calibrationwindow=self,
                calibration_type=self.calibration_type,
                calibration_key=self.calibration_key,
                client_thread=client_thread,
                timeout=600,
            )
        except Exception as exc:
            logger.exception(f"启动标定命令执行失败: {exc}")

    def closeEvent(self, event) -> None:
        """窗口关闭事件：确保停止标定线程，避免 QThread 残留导致崩溃。

        设计目标：
        - 优先尝试优雅停止（发送 Ctrl+C、关闭 channel），等待线程结束。
        - 如仍在运行，进行强制终止以保证不会出现 `QThread: Destroyed while thread is still running`。
        """
        try:
            # 若已进入最终关闭阶段：直接接受关闭事件，避免重复执行停止逻辑
            if getattr(self, "_final_close_ready", False):
                logger.info("CalibrationActionWindow closeEvent: final close ready, accepting")
                # 释放连接持有者，不强制断开（若无其他持有者则自动断开）
                try:
                    if self.vehicle_connection_manager and hasattr(self.vehicle_connection_manager, 'release'):
                        self.vehicle_connection_manager.release(self)
                        logger.info("CalibrationActionWindow: 已释放连接持有者")
                except Exception as e:
                    logger.warning(f"CalibrationActionWindow: 释放连接持有者失败: {e}")
                try:
                    self._closing_in_progress = False
                except Exception:
                    pass
                super().closeEvent(event)
                return

            # 防重入：关闭流程进行中时忽略重复触发
            if getattr(self, "_closing_in_progress", False):
                logger.info("CalibrationActionWindow closeEvent: closing in progress, ignoring re-entry")
                event.ignore()
                return

            # 若存在运行中的工作线程，则在后台线程执行停止流程
            if self._calibration_executor and self._calibration_executor.is_running():
                logger.info("CalibrationActionWindow closeEvent: worker running, stop in background thread")
                try:
                    self.append_log_safe("正在后台停止标定进程，请稍候...")
                except Exception:
                    pass
                try:
                    self._closing_in_progress = True
                    # 启动关闭工作线程（避免UI卡顿）
                    self._close_worker = CalibrationCloseWorker(self._calibration_executor, parent=self)
                    self._close_worker.close_done.connect(self._finalize_close_after_stop_threaded)
                    self._close_worker.start()
                except Exception:
                    # 若线程启动失败，则回退为兜底定时器
                    try:
                        if hasattr(self, "_close_fallback_timer") and self._close_fallback_timer:
                            self._close_fallback_timer.start(2000)
                    except Exception:
                        pass
                event.ignore()
                return

            # 若无运行线程或已停止：直接进入最终关闭阶段（仍在后台做安全清理）
            if self._calibration_executor:
                try:
                    logger.info("CalibrationActionWindow closeEvent: worker not running, forcing stop for safety")
                    self._calibration_executor.stop(force=True)
                except Exception:
                    pass
            try:
                self._closing_in_progress = True
            except Exception:
                pass
            # 设置最终关闭标志并触发一次 close，第二次 closeEvent 将直接接受
            try:
                self._final_close_ready = True
            except Exception:
                pass
            try:
                self.close()
            except Exception:
                pass
            event.ignore()
            return
        except Exception as exc:
            logger.exception(f"窗口关闭时停止标定线程失败: {exc}")
        # 默认行为：避免重复关闭逻辑，这里直接忽略（由上面的流程控制）
        try:
            event.ignore()
        except Exception:
            pass

    def _finalize_close_after_stop(self) -> None:
        """工作线程停止后最终关闭窗口（避免线程尚在运行时销毁）。"""
        try:
            logger.info("CalibrationActionWindow: worker_stopped received, finalizing close")
            # 双保险：再次强制停止，随后执行关闭
            if self._calibration_executor:
                try:
                    self._calibration_executor.stop(force=True)
                except Exception:
                    pass
            # 清理执行器，断开与窗口生命周期的关联
            self._cleanup_executor()
        except Exception:
            pass
        # 进入最终关闭阶段：设置标志并再次触发 close（此时 closeEvent 将直接接受）
        try:
            self._final_close_ready = True
        except Exception:
            pass
        try:
            self.close()
        except Exception:
            pass

    def _finalize_close_after_stop_threaded(self) -> None:
        """后台停止流程完成后收尾并最终关闭窗口（线程版）。"""
        try:
            logger.info("CalibrationActionWindow: background stop finished, finalizing close")
            # 双保险：确保执行器强制停止与清理
            if self._calibration_executor:
                try:
                    self._calibration_executor.stop(force=True)
                except Exception:
                    pass
            self._cleanup_executor()
        except Exception:
            pass
        # 标记最终关闭阶段并触发关闭
        try:
            self._final_close_ready = True
            self._closing_in_progress = False
        except Exception:
            pass
        try:
            self.close()
        except Exception:
            pass

    def _force_close_if_still_running(self) -> None:
        """兜底：若工作线程仍在运行，则强制终止并完成关闭。"""
        try:
            if self._calibration_executor and self._calibration_executor.is_running():
                logger.warning("CalibrationActionWindow: worker still running after timeout, forcing stop")
                try:
                    self._calibration_executor.stop(force=True)
                except Exception:
                    pass
            # 清理执行器并关闭窗口
            self._cleanup_executor()
            # 允许最终关闭：重置防重入标志，避免 closeEvent 被忽略
            try:
                self._closing_in_progress = False
            except Exception:
                pass
            try:
                self.close()
            except Exception:
                pass
        except Exception:
            pass

    def _cleanup_executor(self) -> None:
        """清理执行器对象，避免对象在窗口销毁时触发线程销毁异常。

        安全策略：
        - 不在工作线程仍运行时删除执行器；否则可能导致 `QThread` 在运行时被析构。
        - 优先尝试强制停止；若仍在运行，则延迟重试清理。
        """
        try:
            if not self._calibration_executor:
                return

            try:
                # 先尝试强制停止（兜底）
                self._calibration_executor.stop(force=True)
            except Exception:
                pass

            # 若线程仍在运行，延迟重试，不立即删除执行器
            try:
                if self._calibration_executor.is_running():
                    # 延迟500ms再次尝试清理，避免立即删除导致QThread崩溃
                    if hasattr(self, "_cleanup_retry_timer") and self._cleanup_retry_timer:
                        self._cleanup_retry_timer.start(500)
                    return
            except Exception:
                pass

            # 线程已停止，安全删除执行器对象
            try:
                self._calibration_executor.deleteLater()
            except Exception:
                pass
            self._calibration_executor = None
        except Exception:
            pass

    def _translate_ui(self) -> None:
        """应用国际化文本。"""
        title = self.language_manager.get_text(self.UI_KEY_WINDOW_TITLE, "Calibration Action Window", category="ui")
        self.setWindowTitle(title)

        placeholder = self.language_manager.get_text(
            self.UI_KEY_LOG_PLACEHOLDER,
            "Command outputs and status...",
            category="ui",
        )
        self.log_view.setPlaceholderText(placeholder)

        # 按钮文本
        if btn := self.buttons.get(self.ACTION_DOWNLOAD_LOG):
            btn.setText(self.language_manager.get_text(self.UI_KEY_DOWNLOAD_LOG, "Download Log", category="ui"))
        if btn := self.buttons.get(self.ACTION_DOWNLOAD_PCD):
            btn.setText(self.language_manager.get_text(self.UI_KEY_DOWNLOAD_PCD, "Download PCD", category="ui"))
        if btn := self.buttons.get(self.ACTION_SHOW_PCD):
            btn.setText(self.language_manager.get_text(self.UI_KEY_SHOW_PCD, "Show PCD", category="ui"))

    def _create_button_for_action(self, action: str) -> QPushButton:
        """根据动作类型构造按钮并绑定事件。"""
        if action == self.ACTION_DOWNLOAD_LOG:
            btn = QPushButton(self)
            btn.setObjectName("buttonDownloadLog")
            self._signal_manager.register_connection(f"clicked_{id(btn)}", btn.clicked, self._on_download_log_clicked, Qt.QueuedConnection)
            self._signal_manager.connect_signal(f"clicked_{id(btn)}")
            return btn
        if action == self.ACTION_DOWNLOAD_PCD:
            btn = QPushButton(self)
            btn.setObjectName("buttonDownloadPcd")
            self._signal_manager.register_connection(f"clicked_{id(btn)}", btn.clicked, self._on_download_pcd_clicked, Qt.QueuedConnection)
            self._signal_manager.connect_signal(f"clicked_{id(btn)}")
            return btn
        if action == self.ACTION_SHOW_PCD:
            btn = QPushButton(self)
            btn.setObjectName("buttonShowPcd")
            self._signal_manager.register_connection(f"clicked_{id(btn)}", btn.clicked, self._on_show_pcd_clicked, Qt.QueuedConnection)
            self._signal_manager.connect_signal(f"clicked_{id(btn)}")
            return btn

        # 未知动作类型兜底：创建一个不可用按钮
        btn = QPushButton(self)
        btn.setObjectName("buttonUnknown")
        btn.setText(action)
        btn.setEnabled(False)
        logger.warning("Unknown action type: %s", action)
        return btn


    # ------------------------
    def append_log_safe(self, text: str) -> None:
        """跨线程调用：安全追加日志文本到显示框。"""
        self.appendLogSignal.emit(text )

    @Slot(str)
    def _append_log(self, text: str) -> None:
        """主线程插入日志文本，并确保自动滚动到底部。"""
        try:
            self.log_view.appendPlainText(text)
            # 自动滚动到底部（避免长文本时看不到最新输出）
            sb = self.log_view.verticalScrollBar()
            if sb is not None:
                sb.setValue(sb.maximum())
        except Exception as exc:
            logger.exception("Failed to append log: %s", exc)

    # ------------------------
    # 按钮事件（当前仅发信号与记录日志，不阻塞UI）
    # ------------------------
    @Slot()
    def _on_download_log_clicked(self) -> None:
        """点击“下载日志”按钮：保存当前日志到本地。"""
        self.actionTriggered.emit(self.ACTION_DOWNLOAD_LOG)
        # 调用下载管理器保存日志（默认下载目录，用户可选择路径）
        try:
            CalibrationDownloadManager.save_log(self)
        except Exception as exc:
            logger.exception("保存日志失败: %s", exc)
        # 兼容外部回调（如需链路记录）
        if self.on_action:
            try:
                self.on_action(self.calibration_key, self.ACTION_DOWNLOAD_LOG)
            except Exception as exc:
                logger.exception("on_action callback failed: %s", exc)
        logger.info(
            "Action triggered: type=%s key=%s action=%s",
            self.calibration_type,
            self.calibration_key,
            self.ACTION_DOWNLOAD_LOG,
        )

    @Slot()
    def _on_download_pcd_clicked(self) -> None:
        """点击“下载pcd”按钮：压缩远程目录并下载到本地。"""
        self.actionTriggered.emit(self.ACTION_DOWNLOAD_PCD)
        # 调用下载管理器执行PCD下载（在后台线程执行，实时显示进度）
        try:
            CalibrationDownloadManager.download_pcd(self.vehicle_connection_manager, self)
        except Exception as exc:
            logger.exception("下载PCD失败: %s", exc)
        # 兼容外部回调（如需链路记录）
        if self.on_action:
            try:
                self.on_action(self.calibration_key, self.ACTION_DOWNLOAD_PCD)
            except Exception as exc:
                logger.exception("on_action callback failed: %s", exc)
        logger.info(
            "Action triggered: type=%s key=%s action=%s",
            self.calibration_type,
            self.calibration_key,
            self.ACTION_DOWNLOAD_PCD,
        )

    @Slot()
    def _on_show_pcd_clicked(self) -> None:
        """点击“显示pcd”按钮：下载对应 PCD 并加载查看。"""
        self.actionTriggered.emit(self.ACTION_SHOW_PCD)
        if self.on_action:
            try:
                self.on_action(self.calibration_key, self.ACTION_SHOW_PCD)
            except Exception as exc:
                logger.exception("on_action callback failed: %s", exc)
        logger.info(
            "Action triggered: type=%s key=%s action=%s",
            self.calibration_type,
            self.calibration_key,
            self.ACTION_SHOW_PCD,
        )
        # 下载并加载 PCD 文件
        try:
            local_paths = self._download_required_pcds()
            if not local_paths:
                try:
                    QMessageBox.warning(
                        self,
                        self.language_manager.get_text("pcd_download_failed_title", "下载失败"),
                        self.language_manager.get_text("pcd_download_failed_message", "未能下载任何 PCD 文件，请检查远程路径与连接状态。"),
                    )
                except Exception:
                    pass
                return
            # 打开查看器并加载文件
            self._open_pcd_viewer()
            try:
                viewer = getattr(self, "_pcd_viewer_window", None)
                if viewer and hasattr(viewer, "add_files"):
                    viewer.add_files(local_paths)
            except Exception as exc:
                logger.exception("加载 PCD 文件到查看器失败: %s", exc)
        except Exception as exc:
            logger.exception("下载或加载 PCD 过程失败: %s", exc)

    def _open_pcd_viewer(self) -> None:
        """打开 PCD 查看器子窗口。

        安全策略：
        - 子窗口设置 WA_DeleteOnClose，避免内存泄漏。
        - 持有弱引用属性，窗口关闭后置空。
        """
        try:
            viewer = getattr(self, "_pcd_viewer_window", None)
            if viewer is None:
                viewer = PCDViewerWindow(self)
                self._pcd_viewer_window = viewer
                try:
                    viewer.setWindowModality(Qt.WindowModal)
                    viewer.destroyed.connect(lambda *args, **kwargs: self._on_pcd_viewer_closed())
                except Exception:
                    pass
            viewer.show()
            viewer.raise_()
            viewer.activateWindow()
            # 不禁用父窗口以免连带子窗口不可交互；使用 WindowModal 即可屏蔽父窗口输入
        except Exception as exc:
            logger.exception("创建或显示PCD查看器失败: %s", exc)

    def _on_pcd_viewer_closed(self) -> None:
        try:
            setattr(self, "_pcd_viewer_window", None)
        except Exception:
            pass

    def _resolve_default_download_dir(self) -> Path:
        p = Path("resource/pcd_download").resolve()
        try:
            p.mkdir(parents=True, exist_ok=True)
        except Exception:
            pass
        return p

    def _download_required_pcds(self) -> List[str]:
        """根据当前类型与键，下载所需 PCD 文件到本地目录并返回路径列表。"""
        local_paths: List[str] = []
        try:
            # 解析目标文件列表
            type_map = self.PCD_FILES_MAP.get(self.calibration_type, {})
            # 对上下激光联合标定的单按钮，根据scan_init_down动态选择PCD文件
            if self.calibration_type == "calibration_menu_laser_joint":
                try:
                    car_check = config.get_param("car_calibration_check") or {}
                    scan_init_down = bool(car_check.get("scan_init_down", True))
                    if scan_init_down:
                        filenames = [
                            "middle_down_aligned_cloud.pcd",
                            "middle_lidar_up_cloud.pcd",
                        ]
                    else:
                        filenames = [
                            "left_down_aligned_cloud.pcd",
                            "left_lidar_up_cloud.pcd",
                            "right_down_aligned_cloud.pcd",
                            "right_lidar_up_cloud.pcd",
                        ]
                except Exception:
                    filenames = [
                        "middle_down_aligned_cloud.pcd",
                        "middle_lidar_up_cloud.pcd",
                    ]
            else:
                filenames = type_map.get(self.calibration_key) or type_map.get("single") or []
            try:
                logger.info(
                    "PCD下载准备: type=%s key=%s files=%s",
                    self.calibration_type,
                    self.calibration_key,
                    filenames,
                )
            except Exception:
                pass
            if not filenames:
                try:
                    logger.warning(
                        "PCD下载目标文件为空: type=%s key=%s",
                        self.calibration_type,
                        self.calibration_key,
                    )
                except Exception:
                    pass
                return []
            # 解析本地下载目录
            download_dir = self._resolve_default_download_dir()
            try:
                logger.info("PCD本地目录: %s", str(download_dir))
            except Exception:
                pass
            # 获取客户端线程
            client_thread = None
            try:
                if self.vehicle_connection_manager and hasattr(self.vehicle_connection_manager, "get_client_thread"):
                    client_thread = self.vehicle_connection_manager.get_client_thread()
            except Exception as exc:
                logger.exception("获取车辆客户端线程失败: %s", exc)
                client_thread = None
            if client_thread is None:
                try:
                    logger.warning("PCD下载未获取客户端线程")
                except Exception:
                    pass
                return []
            # 远程基础目录
            remote_base = "/mooe/log/sensor_calibration/"
            failed: List[str] = []
            for name in filenames:
                try:
                    remote_path = f"{remote_base}{name}"
                    local_path = str((download_dir / name).resolve())
                    try:
                        logger.info("开始下载PCD: %s -> %s", remote_path, local_path)
                    except Exception:
                        pass
                    ok = False
                    try:
                        ok = bool(client_thread.download_file_direct(remote_path, local_path))
                    except Exception as exc:
                        logger.exception("下载 PCD 失败: %s", exc)
                        ok = False
                    if ok:
                        local_paths.append(local_path)
                        try:
                            logger.info("PCD下载成功: %s 保存到 %s", name, local_path)
                        except Exception:
                            pass
                    else:
                        failed.append(name)
                        try:
                            logger.warning("PCD下载失败: %s (remote=%s)", name, remote_path)
                        except Exception:
                            pass
                except Exception:
                    failed.append(name)
                    try:
                        logger.warning("PCD下载异常: %s", name)
                    except Exception:
                        pass
            # 若存在失败项，弹窗提示
            if failed:
                try:
                    msg = self.language_manager.get_text(
                        "pcd_download_partial_failed",
                        "部分文件下载失败：\n"
                    ) + "\n".join(failed)
                    QMessageBox.warning(
                        self,
                        self.language_manager.get_text("pcd_download_failed_title", "下载失败"),
                        msg,
                    )
                except Exception:
                    pass
            try:
                logger.info("PCD下载完成: 成功=%d 失败=%d", len(local_paths), len(failed))
            except Exception:
                pass
            return local_paths
        except Exception:
            return local_paths
